<template>
  <div class="com-flow-chart"></div>
</template>
<script lang="ts">
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'
import SVG, { ConnectableObject } from 'svg.js'
import 'svg.draggy.js'
import 'svg.connectable.js'
import { read } from 'fs';
import { StateConfigType, FlowEditMode, StateConfig, LinkConfig } from './com-flow-chart-types'
import _ from 'lodash'
import { types } from 'util';

const GRID_WIDTH = 20
const FLOW_KEY = 'flowKey'
const FLOW_LABEL = 'flowLabel'

const COLORS = [
  '#C2185B',
  '#E91E63',
  '#FF5252',
  '#53ff56',
  '#9a6bb1',
  '#c31967',
]

let counter = 0

function closestPoint (x: number): number {
  const quotient = x % GRID_WIDTH
  if (quotient > GRID_WIDTH / 2) {
    return x - quotient + GRID_WIDTH
  } else {
    return x - quotient
  }
}

function randomColor (colors: string[]): string {
  const len = colors.length
  const idx = Math.floor(Math.random() * len)
  return colors[idx]
}

function drawBackground (svg: SVG.Doc) {
  const backgroundImage = svg.pattern(GRID_WIDTH, GRID_WIDTH, pattern => {
    pattern
      .rect(20, 20)
      .stroke({
        width: 1,
        dasharray: '1 4',
        opacity: 0.5,
      })
      .fill('none')
  })
  const background = svg.rect(svg.width(), svg.height())
  background.fill(backgroundImage)
}

/**
 * 画一个 group 并将其位置定位的到指定位置.
 * 
 * @param svg 父元素
 * @param vue 当前vue实例
 * @param key 节点唯一标识
 * @param label 节点显示名称
 * @param x 节点初始位置x
 * @param y 节点初始位置y
 * 
 * @returns 新建的 group 节点
 */
function drawNode (
  svg: SVG.Container,
  vue: ComFlowChart,
  key: string,
  label?: string,
  x?: number,
  y?: number) {
  const tx = x ? x : 0
  const ty = y ? y : 0
  const node = svg.group().translate(tx, ty)
  node.on('mousedown', function (this: SVG.Element, event) {
    nodeMouseDonw(vue, this, event)
  })
  node.on('mouseup', function (this: SVG.Element, event) {
    nodeMouseUp(vue, this, event)
  })
  node[FLOW_KEY] = key
  node[FLOW_LABEL] = label
  return node
}

function nodeMouseDonw (vue: ComFlowChart, svg: SVG.Element, e: MouseEvent) {
  const key = svg[FLOW_KEY]
  vue.clearSelect()
  svg.addClass('active-node')
  vue.startLinkKey = key
  vue.$emit('clickNode', key)
  console.log('nodeMouseDonw ' + key)
}

function nodeMouseUp (vue: ComFlowChart, svg: SVG.Element, e: MouseEvent) {
  const key = svg[FLOW_KEY]
  const clickKey = vue.startLinkKey
  if (vue.mode === 'l' && clickKey && key !== clickKey) {
    console.log('nodeMouseUp ' + clickKey + ' ---->' + key)
    const from = findStateInFlow(clickKey, vue)
    const to = findStateInFlow(key, vue)
    const connect = from.connectable({
      container: vue.linkGroup,
      markers: vue.markerGroup,
    }, to, ).setLineColor('#5D4037')
    const linkKey = 'tmp' + counter++
    vue.connects[linkKey] = connect
    vue.$emit('linkNode', { key: linkKey, from: clickKey, to: key })
  }
}

function onDragEnd (this: SVG.Element, event) {
  this.x(closestPoint(this.x()))
  this.y(closestPoint(this.y()))
}

function draggable (svg: SVG.Element, vue: ComFlowChart) {
  const range = moveRange(vue)
  svg.draggy(range.bind(svg))
  svg.on('dragend', onDragEnd)
}

function unDraggable (svg: SVG.Element) {
  if (svg.fixed) {
    svg.fixed()
    delete svg.fixed
  }
  svg.off('dragend', onDragEnd)
}

function moveRange (vue: ComFlowChart) {
  return function (this: SVG.Element, x: number, y: number) {
    const bbx = vue.svg.bbox()
    return {
      x: x >= 0 && x <= bbx.w - this.startPosition.width,
      y: y >= 0 && y <= bbx.h - this.startPosition.height,
    }
  }
}

function findStateInFlow (key: string, flow: ComFlowChart): SVG.Element {
  if (flow.start && flow.start[FLOW_KEY] === key) {
    return flow.start
  } else if (flow.end && flow.end[FLOW_KEY] === key) {
    return flow.end
  } else {
    return flow.nodes[key]
  }
}

function drawStart (
  svg: SVG.Container,
  vue: ComFlowChart,
  key: string,
  label?: string,
  x?: number,
  y?: number,
): SVG.Element {
  const start = drawNode(svg, vue, key, label, x, y)
  start.circle(40).fill('#93a7ff')
  return start
}

function drawState (
  svg: SVG.Container,
  vue: ComFlowChart,
  key: string,
  label?: string,
  x?: number,
  y?: number,
): SVG.Element {
  const ty = y ? y : 0
  const state = drawNode(svg, vue, key, label, x, y)
  state.rect(100, 80).radius(10, 10).fill(randomColor(COLORS))
  return state
}

function bindClickFlowFunc (vue: ComFlowChart) {
  return function (this: SVG.Doc, e: MouseEvent) {
    if (vue.mode === 'i') {
      const key = 'tmp' + counter++
      const { offsetX, offsetY } = e
      const x = closestPoint(offsetX)
      const y = closestPoint(offsetY)
      const drawed = drawState(vue.nodeGroup, vue, key, '', x, y)
      vue.nodes[key] = drawed
      vue.$emit('addState', { key: key, type: 'state', label: '', x: x, y: y })
    }
  }
}

/**
 * @class
 * @component com-flow-chart
 * @description  flow 展示框，负责展示，拖动，链接，缩放
 */
@Component({ name: 'com-flow-chart' })
export default class ComFlowChart extends Vue {

  @Prop({ type: Number, default: 400 })
  readonly canvasHeight: number
  @Prop({ type: Number })
  readonly canvasWidth: number
  @Prop({ type: Number, default: 0 })
  readonly viewX: number
  @Prop({ type: Number, default: 0 })
  readonly viewY: number
  @Prop({ type: Number, default: 400 })
  readonly viewHeight: number
  @Prop({ type: Number, default: 600 })
  readonly viewWidth: number
  @Prop({ type: Array })
  readonly states: Array<StateConfig>
  @Prop({ type: Array })
  readonly links: Array<LinkConfig>

  mode: FlowEditMode = 'v'
  startLinkKey: string

  svg: SVG.Doc
  linkGroup: SVG.G
  markerGroup: SVG.G
  nodeGroup: SVG.G

  start: SVG.Element
  end: SVG.Element
  nodes: { [key: string]: SVG.Element } = {}
  connects: { [key: string]: ConnectableObject } = {}

  mounted () {
    const el = this.$el
    if (SVG.supported) {
      const svg = SVG(this.$el).size(this.canvasWidth || '100%', this.canvasHeight)
      svg.on('click', bindClickFlowFunc(this))
      drawBackground(svg)
      this.linkGroup = svg.group()
      this.markerGroup = svg.group()
      this.nodeGroup = svg.group()
      this.svg = svg
      const bbbox = svg.bbox()
      this.reset()
    } else {
      this.showSvgNotSupport()
    }
  }

  reset () {
    this.linkGroup.clear()
    this.markerGroup.clear()
    this.nodeGroup.clear()
    this.start = null
    this.nodes = {}
    this.connects = {}
    this.end = null
    if (this.states) {
      this.states.forEach(state => {
        const { key, label, x, y, type } = state
        if (type === 'start' || type === 'end') {
          const node = drawStart(this.nodeGroup, this, key, label, x, y, )
          this[type] = node
        } else {
          this.nodes[state.key] = drawState(this.nodeGroup, this, key, label, x, y, )
        }
      })
    }

    if (this.links) {
      this.links.forEach(link => {
        const from = findStateInFlow(link.from, this)
        const to = findStateInFlow(link.to, this)
        const connect = from.connectable({
          container: this.linkGroup,
          markers: this.markerGroup,
        }, to, ).setLineColor('#5D4037')
        this.connects[link.key] = connect
      })
    }
    this.modeChange()
  }

  editMode () {
    this.mode = 'i'
    this.modeChange()
  }

  viewMode () {
    this.mode = 'v'
    this.modeChange()
  }

  linkMode () {
    this.mode = 'l'
    this.modeChange()
  }

  private modeChange () {
    if (this.mode === 'v') {
      _.forIn(this.nodes, (val) => {
        draggable(val, this)
      })
    } else {
      _.forIn(this.nodes, (val) => {
        unDraggable(val)
      })
    }
  }

  clearSelect () {
    this.start.removeClass('active-node')
    const nodes = this.nodes
    Object.keys(nodes).forEach(key => {
      nodes[key].removeClass('active-node')
    })
  }

  showSvgNotSupport () {
    this.$el.style.textAlign = 'center'
    this.$el.innerHTML = 'SVG not supported.'
  }
}
</script>
<style lang="less">
@import url('../../styles/variable.less');

.com-flow-chart {
  overflow: auto;
  width: 100%;

  .active-node {
    stroke: #000;
    stroke-dasharray: 4px;
  }
}
</style>
